﻿using System;

using System.Collections.Generic;
using System.Text;
using Silvermoon.Controls.Base;
using System.Drawing;
using Silvermoon.Controls.Classes;
using Silvermoon.Animations;
using System.Diagnostics;
using Silvermoon.Controls.Interfaces;
using Silvermoon.Controls.Sys;
using Silvermoon.Visuals;

namespace Silvermoon.Controls
{
    /// <summary>
    /// <see cref="T:ListViewBase"/> control that has a focus lens.
    /// </summary>
    public class LensView : ListViewBase
    {
        #region fields;

        protected StretchTransform ItemScale = new StretchTransform();
        private int lensHeight = 96;
        private Lens lens;
        private bool locked = false;
        private List<ILensItem> itemsInLens = new List<ILensItem>();
        private bool keepLensCentered = true;
        private PictureBox lensImage;
        private Rectangle lensImageBounds = new Rectangle(-80 - 8, -40, 80, 80);
        private Animation sync1, sync2;


        /// <summary>
        /// contains the items that are currently inside the lens.
        /// </summary>
        LensItemsContainer lensItemsContainer;
        private int lensOffset = 0;

        /// <summary>
        /// a cache for item containers that do not implement <see cref="T:ILensItem"/>. to attach the scale factor and offset.
        /// </summary>
        private List<Panel> lensItemCache;

        #endregion
        #region ctor

        public LensView()
            : base()
        {
            lensItemsContainer = new LensItemsContainer(this) { Margin = new Margin(0, 0, 0, null) };
            lens = new Lens(this);
        }

        public override void Dispose()
        {
            itemsInLens.Clear();
            if (lensItemCache != null)
            {
                foreach (Control c in lensItemCache) c.Dispose();
                lensItemCache.Clear();
            }
            if (lensImage != null) lensImage.Dispose();
            lensImage = null;
            lens.Dispose();
            lensItemsContainer.Dispose();
            SubControls.Clear();
            base.Dispose();
        }
        #endregion
        #region props

        /// <summary>
        /// Gets or sets the bounds for an optional LensImage that appears above the lens. The location is relative to the right top corner of the lens item.
        /// </summary>
        public Rectangle LensImageBounds
        {
            get { return lensImageBounds; }
            set
            {
                if (lensImageBounds != value)
                {
                    lensImageBounds = value;
                    Invalidate(InvalidationFlag.LayoutAppearance);
                }
            }
        }

        /// <summary>
        /// Gets or sets whether the top of the item(s) in the lens are fixed with the lens, or relative.
        /// </summary>
        public bool Locked { get { return locked; } set { if (locked != value) { locked = value; Invalidate(InvalidationFlag.LayoutAppearance); } } }

        /// <summary>
        /// Gets or sets whether to keep the lens centered after dragging.
        /// </summary>
        public bool KeepLensCentered
        {
            get { return keepLensCentered; }
            set
            {
                if (keepLensCentered != value)
                {
                    keepLensCentered = value;
                    if (value) EnsureValidOffset(true);
                }

            }
        }

        /// <summary>
        /// Gets or sets the zoom factor for an item that is at least partially in the lens
        /// This value is only relevant for items that do not implement <see cref="T:ILensItem"/>.
        /// </summary>
        //public float ZoomFactor { get { return zoomFactor; } set { if (zoomFactor != value) { OnZoomFactorChanged(value); } } }
        public float ZoomFactor { get { return ItemHeight != 0 ? ((float)LensHeight) / ((float)ItemHeight) : LensHeight; } }

        /// <summary>
        /// Gets or sets the height of the lens.
        /// </summary>
        public int LensHeight { get { return lensHeight; } set { if (lensHeight != value) { OnLensHeightChanged(value); } } }

        /// <summary>
        /// Gets the outher height of the lens including drop shadows.
        /// </summary>
        public int OuterLensHeight { get { return LensHeight * 128 / 84; } }


        /// <summary>
        /// Gets the boundaries of the current lens.
        /// </summary>
        public Rectangle LensBounds { get; protected internal set; }

        /// <summary>
        /// Gets or sets the Y offset for the lens
        /// </summary>
        public int LensOffset { get { return lensOffset; } set { if (lensOffset != value) OnLensOffsetChanged(value); } }

        #endregion
        #region methods

        /// <summary>
        /// After the control is initialized, we can check for the initial LensOffset to ensure a valid value and a valid TopOffset:
        /// </summary>
        protected override void Initialized()
        {
            base.Initialized();
            LensOffset = ValidateLensOffset(LensOffset);
            EnsureValidOffset(false);
        }

        protected virtual void OnLensOffsetChanged(int value)
        {
            if (!CheckFlag(InvalidationFlag.Uninitialized))
            {
                value = ValidateLensOffset(value);
            }

            lensOffset = value;
            Invalidate(InvalidationFlag.LayoutAppearance);
        }

        private int ValidateLensOffset(int value)
        {
            int top = -TopOffset;
            if (top > 0) top = 0;
            int bottom = TotalHeight - TopOffset;
            if (bottom < (ClientHeight - LensHeight)) bottom = ClientHeight - LensHeight;
            if (value > bottom) value = bottom;
            if (value < top) value = top;
            return value;
        }


        protected virtual void OnLensHeightChanged(int value)
        {
            if (value < 16) throw new ArgumentOutOfRangeException("LensHeight", "must not be less than 16.");
            lensHeight = value;
            Invalidate(InvalidationFlag.LayoutAppearance);
        }

        /// <summary>
        /// Gets or sets whether to show the scrollbar.
        /// </summary>
        /// <value></value>
        public override bool ShowScrollbar
        {
            get
            {
                // scrollbar's never visible:
                return false;
            }
            set { }
        }

        protected override void PreLayout()
        {
            base.PreLayout();

            if (CheckFlag(InvalidationFlag.LayoutAppearance | InvalidationFlag.Size))
            {
                int itemHeight = LensHeight;
                ItemScale.X = ItemScale.Y = ZoomFactor;
                LensBounds = new Rectangle(0, LensOffset, Width, itemHeight);
                //LensBounds = new Rectangle(0, LensOffset, Width, 120);
            }
        }

        /// <summary>
        /// Creates and/or adjusts all the controls that are visible at the current state.
        /// </summary>
        protected override void OnLayoutChildControls()
        {
            lensItemsContainer.Height = LensHeight;
            base.OnLayoutChildControls();
            foreach (Control c in SubControls)
            {
                ItemBase ib = c as ItemBase;
                if (ib != null) ib.ActualBounds = Rectangle.Empty;
            }
            if (ItemCount > 0) BuildLensItems();
        }


        /// <summary>
        /// Building the items that are inside the lens.
        /// This method is using the fact that it is possible to add a control to more than one parent control. 
        /// To adjust the different location in each parent, the control is embedded in a container panel that has a TranslateTransform that transforms the location of the control
        /// to zero, and also zooms the control.
        /// </summary>
        private void BuildLensItems()
        {
            Rectangle lensBounds = LensBounds;
            Lens lens = this.lens;
            bool itemAttached = false;

            lensItemsContainer.Controls.Clear();
            lens.Control = lensItemsContainer;
            lens.Bounds = lensBounds;
            lens.Height = OuterLensHeight;
            SubControls.Add(lens);
            int lt = Math.Max(lensBounds.Top, 0);
            int itemIndex = Items.GetItemIndexFromTop(lt + TopOffset);

            List<ILensItem> lensItems = new List<ILensItem>();

            AddDeltaToItems(itemIndex);

            if (lensItemCache == null) lensItemCache = new List<Panel>();

            float zf = ZoomFactor;
            float max = LensHeight * zf;
            if (itemIndex < ItemCount)
            {
                ItemBase item = GetItem(itemIndex);

                int topOffset = Items.GetOffset(itemIndex);
                float top = ((topOffset - TopOffset) - lensBounds.Top) * zf;
                for (; ; )
                {
                    if (item.Top < lensBounds.Bottom)
                    {
                        if (item.Top < lensBounds.Top)
                        {
                            // clip to bounds so that the item does not overlap in the lens area:
                            item.ActualBounds = new Rectangle(item.Left, item.Top, item.Width, lensBounds.Top - item.Top);
                        }
                        else if (item.Bottom > lensBounds.Bottom)
                        {
                            // clip to bounds so that the item does not overlap in the lens area:
                            item.ActualBounds = new Rectangle(item.Left, lensBounds.Bottom, item.Width, item.Bottom - lensBounds.Bottom);
                        }
                        else
                        {
                            // the item is completely in the lens area, so remove it from the background to avoid overlapping:
                            SubControls.Remove(item);
                        }

                        if (!locked)
                        {
                            AddToLens(lensItems, item, top);
                            if (!itemAttached) AddLensItem(item);
                            itemAttached = true;
                        }
                    }
                    int h = (int)(zf * item.Height + 0.5f);
                    top += h;

                    if (top > max) break;

                    itemIndex++;
                    if (itemIndex >= ItemCount) break;
                    item = GetItem(itemIndex);
                }
            }
            {
                int index = GetItemIndexInLens();
                ItemBase item = GetItem(index);
                if (locked) AddToLens(lensItems, item, 0f);
                AddLensItem(item);
            }
            SendLensItemNotifications(lensItems);
        }

        private void AddLensItem(ItemBase item)
        {
            ILensItemImage lensItemImage = item as ILensItemImage;
            if (lensItemImage != null)
            {
                if (lensImage == null) lensImage = new PictureBox { Color = Color.White };
                lensImage.Image = lensItemImage.LensImage;
                if (!SubControls.Contains(lensImage)) SubControls.Add(lensImage);
                Rectangle r = LensBounds;
                Rectangle ir = LensImageBounds;
                lensImage.Bounds = new Rectangle(LensBounds.Right + ir.Left, LensBounds.Top + ir.Top, ir.Width, ir.Height);

            }
            else
            {
                if (lensImage != null) SubControls.Remove(lensImage);
            }
        }

        private void AddToLens(List<ILensItem> lensItems, ItemBase item, float top)
        {
            float zf = ZoomFactor;
            ILensItem lensItem = item as ILensItem;
            if (lensItem == null)
            {
                Panel zoom = GetNextZoomPanel(lensItems.Count);
                zoom.Controls.Add(item);
                TranslateTransform t = new TranslateTransform { OffsetY = -item.Top };
                zoom.Transformations = new Transformation[] { ItemScale, t };
                zoom.Bounds = new Rectangle(0, (int)(top + 0.5f), Width, item.Height);
                zoom.Flags |= ControlFlags.IgnoreChildClipping;
                lensItemsContainer.Controls.Add(zoom);
            }
            else
            {
                Control c = lensItem.LensItem;
                lensItemsContainer.Controls.Add(c);
                lensItems.Add(lensItem);

                int y = (int)(top + 0.5f);
                c.Bounds = new Rectangle(0, y, Width, (int)(item.Height * zf + 0.5f));
            }
        }

        /// <summary>
        /// calls AddedToLens and RemovedFromLens for all ILensItems if necassary.
        /// </summary>
        /// <param name="lensItems">A list of ILensItems that are currently inside the lens.</param>
        private void SendLensItemNotifications(List<ILensItem> lensItems)
        {
            foreach (ILensItem c in lensItems)
            {
                if (!itemsInLens.Contains(c))
                {
                    c.AddedToLens();
                    itemsInLens.Add(c);
                }
            }

            foreach (ILensItem c in itemsInLens.ToArray())
            {
                if (!lensItems.Contains(c))
                {
                    c.RemovedFromLens();
                    itemsInLens.Remove(c);
                }
            }
        }

        /// <summary>
        /// Some items inside the lens would disappear, so although not realistic, an offset is added to all items under the glass to make them visible:
        /// </summary>
        /// <param name="itemIndex">The index of the first item partially under the glass.</param>
        private void AddDeltaToItems(int itemIndex)
        {
            if (itemIndex < ItemCount)
            {
                Rectangle r = LensBounds;
                ItemBase item = GetItem(itemIndex);
                int delta = LensHeight - item.Height;
                int index = SubControls.IndexOf(item);
                if (index >= 0)
                {
                    for (int i = index; i < SubControls.Count; i++)
                    {
                        item = SubControls[i] as ItemBase;
                        if (item != null && item.Top >= r.Top)
                        {
                            item.Location = new Point(item.X, item.Y + delta);
                        }
                    }
                }
            }
        }

        private Panel GetNextZoomPanel(int magIndex)
        {
            Panel zoom;
            if (magIndex >= lensItemCache.Count)
            {
                zoom = new Panel();
                lensItemCache.Add(zoom);
            }
            else
            {
                zoom = lensItemCache[magIndex];
                zoom.Controls.Clear();
            }
            return zoom;
        }

        private int GetItemIndexInLens()
        {
            if (ItemCount > 0)
            {
                int top = LensBounds.Top;
                int itemIndex = Items.GetItemIndexFromTop(TopOffset + top);
                if (itemIndex >= ItemCount) itemIndex = ItemCount - 1;
                if (itemIndex < 0) itemIndex = 0;
                int dy = GetLensItemOffset(itemIndex);

                if (dy < 0 && itemIndex < ItemCount - 1)
                {
                    int ftop = (int)(-dy * ZoomFactor + .5f);
                    if (ftop > (LensHeight / 2)) itemIndex++;
                }
                return itemIndex;
            }
            else return -1;
        }

        private int GetLensItemOffset(int index)
        {
            int topOffset = Items.GetOffset(index);
            int top = LensBounds.Top;
            int offset = topOffset - top - TopOffset;
            return offset;
        }

        public override void EnsureValidOffset(bool animate)
        {
            if (ItemCount > 0)
            {
                int itemIndex = GetItemIndexInLens();
                SetLensUnderItemIndex(animate, itemIndex);
            }
            else TopOffset = 0;
        }

        protected override void StopScrolling()
        {
            if (sync1 != null)
            {
                sync1.Abort();
                sync1 = null;
            }
            if (sync2 != null)
            {
                sync2.Abort();
                sync2 = null;
            }
            base.StopScrolling();
        }


        private void SetLensUnderItemIndex(bool animate, int itemIndex)
        {
            StopScrolling();

            // must call PreLayout to ensure that the boundaries have all correct values:
            PreLayout();
            if (CheckFlag(InvalidationFlag.Uninitialized)) animate = false;
            int dy = GetLensItemOffset(itemIndex);
            int lensOffset = KeepLensCentered ? (Height - LensHeight) / 2 : LensOffset;
            int dl = lensOffset - LensOffset;
            dy -= dl;
            if (dy != 0 || dl != 0)
            {
                if (animate)
                {
                    if (lensOffset != LensOffset)
                    {

                        Animation a1 = new Animation(350, LensOffset, lensOffset, 0);
                        a1.Progress += (o, e) => LensOffset = e.Value;
                        sync1 = a1;
                        a1.InvokeAsync();
                    }
                    if (dy != 0)
                    {
                        Animation a2 = new Animation(350, TopOffset, TopOffset + dy, 0);
                        a2.Progress += (o, e) => TopOffset = e.Value;
                        sync2 = a2;
                        a2.InvokeAsync();
                    }
                }
                else
                {
                    TopOffset += dy;
                    LensOffset = lensOffset;
                }
            }
            SelectedItem = GetItem(itemIndex);
        }

        /// <summary>
        /// Scroll the marker to the specified position and also scroll TopOffset if necassary.
        /// </summary>
        /// <param name="top"></param>
        public void ScrollMarkerTo(int top)
        {
            StopScrolling();

            int topOffset = TopOffset;
            if (top < 0)
            {
                topOffset += top;
                top = 0;
            }
            else if ((top + LensHeight > ClientHeight))
            {
                topOffset += (top + LensHeight - ClientHeight);
                top += (TopOffset - topOffset);
            }

            if (LensOffset != top)
            {
                Animation animation = new Animation(350, LensOffset, top, 0);
                animation.Progress += (o, e) => LensOffset = e.Value;
                sync1 = animation;
                animation.InvokeAsync();
            }

            if (topOffset != TopOffset)
            {
                Animation a2 = new Animation(350, TopOffset, topOffset, 0);
                a2.Progress += (o, e) => TopOffset = e.Value;
                sync2 = a2;
                a2.InvokeAsync();
            }
        }


        protected override void OnSelectedItemChanged(ItemBase oldItem, ItemBase newItem)
        {
            // don't change this order, otherwise it would lead to a dead loop:
            base.OnSelectedItemChanged(oldItem, newItem);
            EnsureValidMarkerPosition(newItem);
        }

        public override void SelectItem(ItemBase item)
        {
            if (this.SelectedItem != null) SelectedItem.OnSelectedChanged();
            if (item != null && SelectedItem != item) item.OnSelectedChanged();
            // do nothing.
        }

        public override bool IsItemSelected(ItemBase item)
        {
            return false;
        }


        public override void OnItemClick(ItemBase item)
        {
            SelectedItem = item;
            base.OnItemClick(item);
        }


        private int EnsureValidMarkerPosition(ItemBase item)
        {
            StopScrolling();
            int index = Items.IndexOf(item);

            if (KeepLensCentered)
            {
                SetLensUnderItemIndex(true, index);
            }
            else
            {

                int top = LensBounds.Top;
                int topOffset = Items.GetOffset(index);
                int dy = topOffset - top - TopOffset;
                ScrollMarkerTo(LensOffset + dy);
            }
            return index;
        }

        public override void EndInit()
        {
            base.EndInit();
        }

        #endregion
    }
}
